﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.ComponentModel;

namespace PasswordProvider.ObjectModel
{
    // ProtectedString is a wrapper class for System.Security.SecureString. SecureString stores its value encrypted, 
    // and in unmanaged memory, (so it can be erased completely when the object is destroyed.) 
    // However, for added security, SecureString does not provide methods to obtain the string value of its contents.
    // This class adds that functionality while maintaining the essential security features of SecureString.
    public sealed class ProtectedString : IDisposable, INotifyPropertyChanged
    {
        #region Fields, Constructors
        private SecureString _value;

        public ProtectedString()
        {
            _value = new SecureString();            
        }

        // Construct with a SecureString
        public ProtectedString(SecureString value)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            _value = value;
        }
        // Construct with a managed string
        public ProtectedString(string value)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            _value = ManageStringToSecureString(value);
        }
        // Construct with an array of utf8 encoded bytes.
        public ProtectedString(byte[] utf8Value)
        {
            _value = ManageStringToSecureString(Encoding.UTF8.GetString(utf8Value));
        }
        #endregion

        #region Comparison 

        /// <summary>
        /// Compare two protected strings, returns true if the internal contents are equal.
        /// </summary>
        public override bool Equals(object obj)
        {
            ProtectedString other = obj as ProtectedString;
            if (other == null)
                return false;

            if(this.Length != other.Length)
                return false;

            return GetUtf8Bytes().SequenceEqual(other.GetUtf8Bytes());
        }
        /// <summary>
        /// Override GetHashCode because of override of Equals.
        /// </summary>
        public override int GetHashCode()
        {
            return _value.GetHashCode();
        }
        /// <summary>
        /// Overload ==
        /// </summary>
        public static bool operator ==(ProtectedString value1, ProtectedString value2)
        {
            return (object)value1!=null ? value1.Equals(value2) : (object)value2==null;
        }
        /// <summary>
        /// Overload ==
        /// </summary>
        public static bool operator !=(ProtectedString value1, ProtectedString value2)
        {
            return (object)value1 != null ? !value1.Equals(value2) : (object)value2 != null;
        }
        
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this._value.Dispose();
        }

        #endregion

        #region INotifyPropertyChanged Members

        // Notifies when the value is changed.
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Public Properties
        
        /// <summary>
        /// Returns the length of the internal SecureString
        /// </summary>
        public int Length
        {
            get { return _value.Length; }
        }

        /// <summary>
        /// Obtain the underlying value as an unprotected, unencrypted, managed string.
        /// </summary>
        public string UnprotectedString
        {
            get
            {           
                return SecureStringToManagedString(_value);
            }
        }
        
        /// <summary>
        /// Retrieve the underlying value as a string, then convert that string to an array of utf8 encoded bytes.
        /// </summary>
        internal byte[] GetUtf8Bytes()
        {
            string value = SecureStringToManagedString(_value);
            return Encoding.UTF8.GetBytes(value);        
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Set the value of the contained secure string.
        /// </summary>
        public void SetValue(SecureString value)
        {
            _value = value;
            RaisePropertyChanged();
        }

        /// <summary>
        /// Set the value with a clear text string. The string is converted immediatly into a secure string.
        /// </summary>
        public void SetValue(string value)
        {
            _value = ManageStringToSecureString(value);
            RaisePropertyChanged();
        }

        // Add a character at the specified index. Raises the PropertyChanged event.
        public void InsertAt(int index, char character)
        {
            _value.InsertAt(index, character);
            RaisePropertyChanged();
        }
        // Remove the character at the specified index. Raises the PropertyChanged event.
        public void RemoveAt(int index)
        {
            _value.RemoveAt(index);
            RaisePropertyChanged();
        }
        
        #endregion

        #region Private Methods

        private static string SecureStringToManagedString(SecureString secure)
        {
            IntPtr p = Marshal.SecureStringToBSTR(secure);
            string value = Marshal.PtrToStringBSTR(p);
            Marshal.ZeroFreeBSTR(p);
            return value;
        }
 
        private static SecureString ManageStringToSecureString(string value)
        {
            SecureString secure = new SecureString();
            if (value != null)
            {
                foreach (char c in value.ToCharArray())
                {
                    secure.AppendChar(c);
                }
            }
            return secure;
        }

        private void RaisePropertyChanged()
        {
            if(PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(null));
        }

        #endregion

    }
}
